import java.net.*;
import javax.net.ssl.*;
import java.util.*;
import java.io.*;
/**
 * This class is a JSSE server that sits and listens for an incoming SSL
 * connection.  Once a connection is made, the server reads a "message" 
 * (defined as a stream terminated by <LF>) and sends back a modified 
 * version of the message to the client before closing the connection.
 * <p>The code to perform the SSL stuff is all in "main" - the use of other
 * methods is limited just to displaying information, so that you should
 * only need to read the "main" code to see what order things are done.
 * <p>Built using JDK1.4 and tested on Windows 2000
 * @author Nick Hudson
 * @version 1.0
*/
public class ExampleSSLServer {

    static Object syncObject = new Object();

    /**
     * Report a message to System.out
     * @param doTrace true if you want the message to appear, false otherwise
     * @param message the message to be sent to System.out
     */
    public static void trace(boolean doTrace, String message) 
    {
	if (doTrace) {
	    synchronized(syncObject) {
		System.out.println(message);
	    }
	}
    }

    /**
     * Display usage information
     */    
    public static void usage() 
    {
	trace(true,"\nUsage: java ExampleSSLServer <port> "
			   + "<ciphers> [trace]");
	trace(true," port        : port number to listen on");
	trace(true," ciphers     : types of cipher suites to enable :-");
	trace(true,"               'c' : suites that use server-cert");
	trace(true,"               'a' : anonymous (no server-cert)");
	trace(true," trace       : (optional) tracing to enable:-");
	trace(true,"               'j' : turn on JSSE debug tracing");
	trace(true,"               't' : turn on program tracing");
    }
    
    /**
     * This method looks at the run-time environment in an attempt to check
     * if things may need setting up for JSSE to work properly.  It reports
     * any areas of concern but does not abort.
     */
    public static void checkEnvironment() 
    {
	// Check that a keystore has been specified.  JSSE doesn't assume a
	// default value for keystore, so the only way we'll find one is if
	// the system properties have been set.
	String keyStoreName = System.getProperty("javax.net.ssl.keyStore");
	if (keyStoreName == null) {
	    // Tell them there may be a problem, but we don't abort; there
	    // will be an SSLException if they try and use non-anonymous
	    // ciphers
	    trace(true,
"\nYou have not specified a keystore, which means that the program will not");
	    trace(true,
"be able to use cipher suites that require a certificate.  To specify a");
	    trace(true,
"keystore, use e.g. :\n");
	    trace(true,
"  java -Djavax.net.ssl.keyStore=\"<file>\" ...\n");
	}
	else {
	    // The user has specified a keystore, check that they have supplied
	    // a password
	    if (System.getProperty("javax.net.ssl.keyStorePassword") == null) {
		trace(true,
"\nYou have not specified a keystorePassword, which means that the if your");
		trace(true,
"keystore has a non-null password, JSSE won't be able to read certificate");
		trace(true,
"information from it - in this case you may see a \"Cannot recover key\".");
		trace(true,
"message.  To specify a keystore password, use e.g. :\n");
		
		    trace(true,
"  java -Djavax.net.ssl.keyStorePassword=\"<passw>\" ...\n");
	    }
	}
    }    

    /**
     * Display various information out of a HandshakeCompletedEvent object.
     * @param event HandshakeCompletedEvent object.
     */
    public static void showHandshakeInfo(HandshakeCompletedEvent event) 
    {
	// This method may be called from a separate thread, so all of
	// the output here in a synchronized block to make sure it doesn't
	// get interspersed with other messages.
	synchronized(syncObject)
        {    
	    trace(true,"Negotiated cipher suite " + event.getCipherSuite());
	    
	    java.security.cert.Certificate lc[] = event.getLocalCertificates();
	    if (lc != null) {
		trace(true,"" + lc.length + " local certificates used :");
		for (int i=0; i<lc.length; i++) {
		    trace(true,"#" + (i+1) + " " + lc[i].toString());
		}
	    }
	    else {
		trace(true,"No local certificates used");
	    }		
	    
	    try {
		lc = event.getPeerCertificates();
		if (lc != null) {
		    trace(true,"" + lc.length + " peer certificates :");
		    for (int i=0; i<lc.length; i++) {
			trace(true,"#" + (i+1) + " " + lc[i].toString());
		    }
		}
	    }
	    catch (SSLPeerUnverifiedException e) {
		trace(true,"Peer's identity has not been verified");
	    }
	    trace(true,""); // write a blank line
	}
	
    }
    
    /**
     * Main method
     * @param args java.lang.String[]
     */
    public static void main(String[] args)
    {
	int portNumber;
	boolean enableAnonSuites = false;
	boolean enableCertSuites = false;
	boolean isTracing = false;
	

	try {
	    portNumber = Integer.parseInt(args[0]);
	    
	    String cipherOptions = args[1].toLowerCase();
	    enableAnonSuites = (cipherOptions.indexOf("a") != -1);
	    enableCertSuites = (cipherOptions.indexOf("c") != -1);
	    
	    // trace argument is optional
	    if (args.length > 2) {
		String traceOptions = args[2].toLowerCase();
		isTracing = (traceOptions.indexOf("t") != -1);
		if (traceOptions.indexOf("j") != -1) 
		    {
			// Enable JSSE tracing
			trace(isTracing, "Enabling JSSE debug tracing");
			System.setProperty("javax.net.debug","ssl");
		    }
	    }
	}
	catch (Exception e) {
	    trace(true,e.toString());
	    // They invoked the program with invalid arguments
	    usage();
	    return;
	}

	checkEnvironment();
	
	
	SSLServerSocketFactory ssf = null;
	SSLServerSocket listenSocket = null;
	
	try {
	    trace(true,"Configuring SSLServerSocketFactory, please wait...");
	    ssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
	    
	    trace(isTracing,"Creating new ServerSocket");
	    listenSocket = (SSLServerSocket)ssf.createServerSocket(portNumber);
	    
	    // Look at each of the supported cipher suites, and build a list
	    // of those that we want to use, based on the user's requirements
	    String[] supportedSuites = listenSocket.getSupportedCipherSuites();
	    HashSet set = new HashSet();
	    
	    trace(isTracing,"Supported cipher suites are:");
	    
	    for (int i=0; i<supportedSuites.length; i++) {
		trace(isTracing,"  " + supportedSuites[i]);
		if (supportedSuites[i].indexOf("anon") != -1) {
		    // This is an anonymous suite
		    if (enableAnonSuites) {
			set.add(supportedSuites[i]);
		    }
		}
		else {
		    // This is not an anonymous suite
		    if (enableCertSuites) {
			set.add(supportedSuites[i]);
		    }
		}
	    }
	    
	    // Build an array of all the suites that we just put on the stack
	    String[] desiredSuites = (String[])set.toArray(new String[0]);
	    listenSocket.setEnabledCipherSuites(desiredSuites);
	    if (isTracing) {
		trace(true,"Enabled cipher suites are:");
		String[] enabledSuites = listenSocket.getEnabledCipherSuites();
		for (int i=0; i<enabledSuites.length; i++) {
		    trace(true,"  " + enabledSuites[i]);
		}
	    }
	    
	    // Enable all supported protocols
	    String[] protocols = listenSocket.getSupportedProtocols();
	    listenSocket.setEnabledProtocols(protocols);

	    if (isTracing) {
		protocols = listenSocket.getEnabledProtocols();
		trace(true,"Enabled protocols are:");
		for (int i=0; i<protocols.length; i++) {
		    trace(true,"  " + protocols[i]);
		}
	    }
	}
	catch (Exception e) {
	    trace(true,"Exception while configuring socket :\n" + e);
	    trace(true,"-- program terminating");
	    return;
	}
	
	// The socket should all be set up now
	trace(true,"...Now listening for clients on port " + portNumber);
	SSLSocket clientSocket = null;
	
	try {

	    // Wait for someone to talk to us
	    clientSocket = (SSLSocket)listenSocket.accept();

	    trace(true,"\nAccepted connection from " 
		  + clientSocket.getInetAddress());
	    
	    if (isTracing) {
		clientSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
		    public void handshakeCompleted(HandshakeCompletedEvent e) 
		    {
			trace(true,"SSL handshake completed : ");
			showHandshakeInfo(e);
		    }
		    });
	    }


	    InputStream inStream = clientSocket.getInputStream();
	    OutputStream outStream = clientSocket.getOutputStream();	    

	    // Read a byte at a time from the socket, looking for 
	    // an EOL which will terminate the message.  Push each
	    // byte into a ByteArrayOutputStream which we can subsequently
	    // turn into a String
	    
	    ByteArrayOutputStream b = new ByteArrayOutputStream();
	    int oneByte;
	    do {
		oneByte = inStream.read();
		if (oneByte == -1) {
		    throw new SocketException("client closed connection");
		}		
		b.write(oneByte);
	    }
	    while (oneByte != 10);

	    
	    // Turn the stream of bytes into a String
	    String message = b.toString();
	    
	    trace(true,"The client sent -->" + message + "<--\n");
	    
	    // And send it back to the client
	    message = "JSSE Server says thank-you for saying : " + message;
	    
	    byte[] reply = message.getBytes();
	    trace(true,"Send to the client:  -->" + message + "<--\n");
	    outStream.write(reply);
	    
	}
	
	catch (Exception e) {
	    trace(true,"Exception while communicating with client :\n" + e);
	    trace(true,"-- program terminating");	    
	    return;
	}
	
	finally {
	    try {
		// "Socket.close()" requires a catch clause
		if (clientSocket != null) {
		    trace(isTracing,"Closing client socket");
		    clientSocket.close();
		}
		
		if (listenSocket != null) {
		    trace(isTracing,"Closing server socket");
		    listenSocket.close();
		}
		
	    }
	    catch (Exception e) {
		trace(true,"Exception when closing sockets:\n" + e.toString());
	    }
	    
	}
    }
}
